/*
 * Copyright 2008 Andrej Herich
 * 
 * http://azetclient.yw.sk/
 * http://azetclient.googlecode.com/
 * 
 * This file is part of AzetClient.
 * 
 * AzetClient is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * AzetClient is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with AzetClient.  If not, see <http://www.gnu.org/licenses/>.
 */

package sk.yw.azetclient.managers;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.log4j.Logger;
import sk.yw.azetclient.model.Buddy;
import sk.yw.azetclient.model.Message;
import sk.yw.azetclient.model.MessageBean;
import sk.yw.azetclient.model.MessageThread;
import sk.yw.azetclient.storage.MessageThreadStorage;
import sk.yw.azetclient.storage.StorageException;

/**
 *
 * @author error216
 */
public class MessageThreadManager {
    
    private static final Logger logger =
            Logger.getLogger(MessageThreadManager.class);
    
    private static final Pattern FILE_NAME_PATTERN =
            Pattern.compile("(.*?)@(.*?)\\.xml");
    
    private Buddy user;
    private int maxId = 0;
    private File storageDir;
    private Set<MessageThread> threads =
            new HashSet<MessageThread>();
    
    private MessageThreadStorage storage =
            new MessageThreadStorage();
    
    private List<MessageThreadManagerListener> listeners =
            new ArrayList<MessageThreadManagerListener>();
    
    private Timer timer = new Timer();
    
    private Lock threadFilesManipulationLock = new ReentrantLock();
    
    private TimerTask storeTask = new TimerTask() {
        @Override
        public void run() {
            storeThreads();
        }
    };
    
    private static MessageThreadManagerListener defaultListener =
            new MessageThreadManagerListener() {

        public void errorOccured(MessageThreadManagerEvent event) {
            try {
                logger.error("", (Throwable) event.getSubject());
            } catch (Throwable throwable) {
                logger.error("Error while retrieving error", throwable);
            }
        }

        public void threadCreated(MessageThreadManagerEvent event) {
        }

        public void threadLoaded(MessageThreadManagerEvent event) {
        }
    };
    
    public MessageThreadManager(Buddy user, String storageDir,
            long storageInterval) {
        if (user == null)
            throw new IllegalArgumentException("Null pointer in user");
        if (storageDir == null)
            throw new IllegalArgumentException("Null pointer in storageDir");
        
        this.user = user;
        
        this.storageDir = new File(storageDir);
        if (!this.storageDir.exists()) {
            this.storageDir.mkdirs();
        }
        
        if (!this.storageDir.isDirectory())
            throw new IllegalArgumentException("storageDir is not a directory");
        
        for (File file : this.storageDir.listFiles()) {
            Matcher matcher = FILE_NAME_PATTERN.matcher(file.getName());
            if (matcher.matches()) {
                try {
                    int id = Integer.parseInt(matcher.group(2));
                    if (id > maxId) maxId = id;
                } catch (NumberFormatException ex) {
                    logger.warn("Invalid message thread id in file name: " + matcher.group(2), ex);
                }
            }
        }
        
        addMessageThreadManagerListener(defaultListener);
        
        scheduleStoring(storageInterval);
    }
    
    public synchronized void addMessageThreadManagerListener(
            MessageThreadManagerListener listener) {
        listeners.add(listener);
    }
    
    public synchronized void removeMessageThreadManagerListener(
            MessageThreadManagerListener listener) {
        if (!listeners.remove(listener)) {
            logger.warn("Unable to remove MessageThreadManagerListener.");
        }
    }
    
    protected void fireErrorOccured(Throwable throwable) {
        MessageThreadManagerEvent event = new MessageThreadManagerEvent(this,
                MessageThreadManagerEvent.Type.ERROR, throwable);
        for (MessageThreadManagerListener listener : listeners) {
            listener.errorOccured(event);
        }
    }
    
    protected void fireThreadCreated(MessageThread thread) {
        MessageThreadManagerEvent event = new MessageThreadManagerEvent(this,
                MessageThreadManagerEvent.Type.THREAD_CREATED, thread);
        for (MessageThreadManagerListener listener : listeners) {
            listener.threadCreated(event);
        }
    }
    
    protected void fireThreadLoaded(MessageThread thread) {
        MessageThreadManagerEvent event = new MessageThreadManagerEvent(this,
                MessageThreadManagerEvent.Type.THREAD_LOADED, thread);
        for (MessageThreadManagerListener listener : listeners) {
            listener.threadLoaded(event);
        }
    }
    
    protected String getFileName(String username, int id) {
        StringBuilder fileName = new StringBuilder(username);
        fileName.append('@');
        fileName.append(id);
        fileName.append(".xml");
        
        return fileName.toString();
    }
    
    protected String getFileName(MessageThread thread) {
        String username = null;
        
        Message firstMessage = thread.getMessages().get(0);
        if (user.equals(thread.getMessages().get(0).getSender())) {
            username = firstMessage.getReceiver().getName();
        } else {
            username = firstMessage.getSender().getName();
        }
        
        return getFileName(username, thread.getId());
    }
    
    private void storeThread(MessageThread thread) {
        if (thread.isEmpty()) return;
        
        String fileName = getFileName(thread);
        try {
            FileOutputStream outputStream = new FileOutputStream(
                    new File(storageDir, fileName));
            storage.save(thread, outputStream);
        } catch (FileNotFoundException ex) {
            logger.error("File not found: " + storageDir.getAbsolutePath()
                    + File.separator + fileName, ex);
            fireErrorOccured(ex);
        } catch (StorageException ex) {
            logger.error("Unable to save thread to file: "
                    + storageDir.getAbsolutePath()
                    + File.separator + fileName, ex);
            fireErrorOccured(ex);
        }
    }
    
    /**
     * Stores all threads which are not stored and sets their status to stored.
     */
    public void storeThreads() {
        threadFilesManipulationLock.lock();
        try {
            logger.info("Message thread storing started.");
            for (MessageThread thread : threads) {
                try {
                    if (!thread.isStored()) {
                        thread.setStored(true);
                        storeThread(thread);
                    }
                } catch (Throwable ex) {
                    logger.error("Store of thread " + thread.getId() + " failed.", ex);
                    fireErrorOccured(ex);
                } 
            }
        } finally {
            threadFilesManipulationLock.unlock();
            logger.info("Message thread storing ended.");
        }
    }
    
    public void scheduleStoring(long storageInterval) {
        timer.schedule(storeTask, storageInterval, storageInterval);
    }
    
    public void cancelAll() {
        timer.cancel();
    }
    
    /**
     * Create new thread managed by this manager.
     * 
     * @return new message thread
     */
    public MessageThread createNewThread() {
        logger.info("Creating new thread.");
        
        maxId++;
        MessageThread thread = new MessageThread(maxId);
        
        threadFilesManipulationLock.lock();
        try {
            threads.add(thread);
            fireThreadCreated(thread);
        } finally {
            threadFilesManipulationLock.unlock();
        }
        
        return thread;
    }
    
    /**
     * Add given message bean to appropriate thread.
     * 
     * @param  message  message bean which you want to find thread for
     * @return message for given messageBean;
     */
    public Message addMessage(MessageBean message) {
        if (message == null)
            throw new IllegalArgumentException("Null pointer in message");
        if (!user.equals(message.getSender())
                && !user.equals(message.getReceiver()))
            throw new IllegalArgumentException("Message " + message.getId()
                    + " is neither sent nor received by user "+ user.getName());
        
        threadFilesManipulationLock.lock();
        boolean locked = true;
        try {
            logger.info("Getting thread for message: " + message.getId());

            MessageThread thread = null;

            // try to add message to one of already managed threads
            for (MessageThread t : threads) {
                if (!t.isEmpty() && t.addMessage(message)) {
                    thread = t;
                    break;
                }
            }

            if (thread != null) {
                threadFilesManipulationLock.unlock();
                locked = false;
                logger.info("Thread found for message: " + message.getId());
                return thread.getLastMessage();
            }
            
            // try to search through stored threads
            String username;
            if (user.equals(message.getSender())) {
                username = message.getReceiver().getName();
            } else {
                username = message.getSender().getName();
            }
            for (int i = maxId; i > 0; i--) {
                if (!threads.contains(new MessageThread(i))) {
                    File file = new File(storageDir, getFileName(username, i));
                    logger.debug("Trying to load file: " + file.getAbsolutePath());
                    if (file.exists()) {
                        try {
                            FileInputStream in = new FileInputStream(file);
                            MessageThread t = storage.loadMinimal(in);
                            if (t.isAddPossible(message)) {
                                in = new FileInputStream(file);
                                t = storage.loadFull(in);
                                if (t.addMessage(message)) {
                                    thread = t;
                                    threads.add(t);
                                    fireThreadLoaded(thread);
                                    break;
                                } else {
                                    logger.error("Inconsistence! After minimal "
                                            + "load add is possible, but after "
                                            + "full load addMessage fails. File name: " + file.getAbsolutePath());
                                    fireErrorOccured(null);
                                }
                            }
                        } catch (FileNotFoundException ex) {
                            logger.error("File " + file.getAbsolutePath() + " exists, but is not found?", ex);
                            fireErrorOccured(ex);
                        } catch (StorageException ex) {
                            logger.error("Load of thread from file " + file.getAbsolutePath() + " failed.", ex);
                            fireErrorOccured(ex);
                        }
                    }
                }
            }

            if (thread != null) {
                threadFilesManipulationLock.unlock();
                locked = false;
                logger.info("Thread found for message: " + message.getId());
                return message.createImmutable(thread);
            }
            
            // we weren't succesful, create new thread
            thread = createNewThread();
            if (!thread.addMessage(message)) {
                logger.error("Unable to add message to empty thread.");
                fireErrorOccured(null);
            }
            
            threadFilesManipulationLock.unlock();
            locked = false;
            logger.info("Thread created for message: " + message.getId());
            return thread.getLastMessage();
            
        } catch (Throwable ex) {
            logger.error("Unexpected error occured while trying to get thread for message: " + message.getId(), ex);
            fireErrorOccured(ex);
        } finally {
            if (locked) threadFilesManipulationLock.unlock();
        }
        return null;
    }
    
    public void closeThread(MessageThread thread) {
        if (thread == null)
            throw new IllegalArgumentException();
        
        threadFilesManipulationLock.lock();
        try {
            logger.info("Closing message thread: " + thread.getId());
            
            threads.remove(thread);
            
            thread.setStored(true);
            storeThread(thread);
            
        } catch (Throwable ex) {
            logger.error("Unexpected error occured while trying to close thread: " + thread.getId(), ex);
            fireErrorOccured(ex);
        } finally {
            threadFilesManipulationLock.unlock();
            logger.debug("Message thread "+ thread.getId() + " closed.");
        }
    }

}
